// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: xguo

#include "coin2/strategy/umm/fill_stats.h"

#include <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <string>

namespace xguo {

namespace {

inline int GetSign(double x) {
  // get sign of a float number
  return (x > 0) * 2 - 1;
}

inline bool IsClose(double x, double y) {
  // is close
  return std::fabs(x - y) < 1e-8;
}

inline double CalcPnlBp(double sell, double buy) {
  // Pnl Bp
  return (sell - buy) / (sell + buy);
}

inline auto ToBuySellProto(const BuySellPair& bsp) {
  coin2::strategy::umm::BuySellPair pair;
  pair.set_buy_timestamp(bsp.buy_timestamp);
  pair.set_sell_timestamp(bsp.sell_timestamp);
  pair.set_fill_buy_price(bsp.fill_buy_price);
  pair.set_fill_sell_price(bsp.fill_sell_price);
  pair.set_fill_qty(bsp.fill_qty);
  pair.set_pnl(bsp.pnl);
  pair.set_pnl_bp(bsp.pnl_bp);
  return pair;
}

}  // namespace

FillStats::FillStats(const FillStatsConfig& config) {
  // constructor
  DCHECK_GT(config.lot_size, 0);
  config_ = config;
}

void FillStats::UpdateByBbo(const Bbo& bbo) {
  const int64_t delta = 5'000'000'000LL;
  set_timestamp(bbo.timestamp);
  bbo_ = bbo;
  num_bbo_++;

  // If there is fill inconsistency, compensate.
  Fill fill;
  if (timestamp() - last_pos_check_time_ > delta) {
    last_pos_check_time_ = timestamp();

    double diff = position_ - buy_qty_ - sell_qty_;
    if (std::fabs(diff) > 1e-5) {
      fill.fill_price = bbo.midp;
      fill.fill_qty = diff;
      UpdateByFill(fill);
      LOG(ERROR) << "Compensate fill/pos inconsistency, "
                 << diff << ", " << fill.fill_price << "/" << fill.fill_qty;
    }
  }
}

void FillStats::UpdateByPos(double pos) {
  position_ = pos;
}

void FillStats::UpdateByFill(const Fill& fill) {
  set_timestamp(fill.timestamp);
  last_fill_timestamp_ = fill.timestamp;

  // Put in the list, update stats.
  fills_.emplace_back(fill);

  const int fill_side = GetSign(fill.fill_qty);
  const double amount = fill.fill_price * fill.fill_qty;
  if (last_fill_side_ != fill_side) {
    consecutive_buy_fill_qty_ = 0;
    consecutive_sell_fill_qty_ = 0;
    last_fill_side_ = fill_side;
  }

  fee_ += fill.fee;
  if (fill_side > 0) {
    buy_qty_ += fill.fill_qty;
    buy_amount_ += amount;
    consecutive_buy_fill_qty_ += fill.fill_qty;
    last_buy_fill_timestamp_ = fill.timestamp;
  } else {
    sell_qty_ += fill.fill_qty;
    sell_amount_ += amount;
    consecutive_sell_fill_qty_ += fill.fill_qty;
    last_sell_fill_timestamp_ = fill.timestamp;
  }

  // Found matches.
  MatchFill(&fills_.back());
}

const Fill* FillStats::highest_fill_sell() const {
  if (sell_fills_.empty()) {
    return nullptr;
  }

  int nlayer = std::floor(-sfqty_ / config_.lot_size);
  CHECK_GE(nlayer, 0) << -sfqty_ << ", " << config_.lot_size;

  if (nlayer == 0 && (-sfqty_) < config_.lot_size * 0.5) {
    return nullptr;
  }

  double s = 1;
  double sum = 0;
  double qty = -sfqty_;
  for (int n = 0; n < nlayer; n++) {
    sum += (config_.lot_size * s);
    s *= (1 - config_.walk_step);
    qty -= config_.lot_size;
  }
  sum += (s * qty);
  double p0 = -sfamt_ / sum;

  sell_fill_.fill_price = p0;
  sell_fill_.fill_qty = config_.lot_size;
  return &sell_fill_;;
}

const Fill* FillStats::lowest_fill_buy() const {
  if (buy_fills_.empty()) {
    return nullptr;
  }

  int nlayer = std::floor(bfqty_ / config_.lot_size);
  CHECK_GE(nlayer, 0);

  if (nlayer == 0 && bfqty_ < config_.lot_size * 0.5) {
    return nullptr;
  }

  double s = 1;
  double sum = 0;
  double qty = bfqty_;
  for (int n = 0; n < nlayer; n++) {
    sum += (config_.lot_size * s);
    s *= (1 + config_.walk_step);
    qty -= config_.lot_size;
  }
  sum += (s * qty);
  double p0 = bfamt_ / sum;

  buy_fill_.fill_price = p0;
  buy_fill_.fill_qty = config_.lot_size;
  return &buy_fill_;
}

void FillStats::MatchFill(Fill* fill) {
  bfamt_ = 0;
  bfqty_ = 0;
  sfamt_ = 0;
  sfqty_ = 0;

  if (fill->fill_qty > 0) {  // Buy fill
    // first find all profitable matches.
    for (auto* sfill : sell_fills_) {
      if (IsClose(fill->fill_qty, 0.0)) {
        break;
      }

      // sell_fill order (big -> small)
      if (fill->fill_price * (1 + walk_step()) < sfill->fill_price) {
        double tmp_qty = fill->fill_qty + sfill->fill_qty;
        double matched_fill_qty = 0.0;
        if (tmp_qty <= 0.0) {
          sfill->fill_qty = tmp_qty;
          matched_fill_qty = fill->fill_qty;
          fill->fill_qty = 0.0;
          CHECK_GT(matched_fill_qty, 0);
        } else {
          fill->fill_qty = tmp_qty;
          matched_fill_qty = -sfill->fill_qty;
          sfill->fill_qty = 0.0;
          CHECK_GT(matched_fill_qty, 0);
        }

        BuySellPair bsp;
        bsp.buy_timestamp = fill->timestamp;
        bsp.sell_timestamp = sfill->timestamp;
        bsp.fill_buy_price = fill->fill_price;
        bsp.fill_sell_price = sfill->fill_price;
        bsp.fill_qty = matched_fill_qty;
        bsp.pnl = (bsp.fill_sell_price - bsp.fill_buy_price) * bsp.fill_qty;
        bsp.pnl_bp = CalcPnlBp(bsp.fill_sell_price, bsp.fill_buy_price);
        // bsp_list_.push_back(bsp);
        CHECK_GE(bsp.pnl * bsp.pnl_bp, 0);

        auto proto = ToBuySellProto(bsp);
        buy_profit_qty_ += bsp.fill_qty;
        buy_profit_pnl_ += bsp.pnl;
        LOG(INFO) << "BuySellProto: (buy-sell-match-profit) " << proto.DebugString();
        LOG(ERROR) << "buy_profit_qty: " << buy_profit_qty_
            << ", buy_profit_pnl: " << buy_profit_pnl_;
      }
    }

    // matches that causes loss
    if (fill->fill_qty > 0.0) {
      for (auto* sfill : sell_fills_) {
        // Ingore those that matched already.
        if (IsClose(sfill->fill_qty, 0.0)) {
          continue;
        }

        if (IsClose(fill->fill_qty, 0.0)) {
          break;
        }

        double tmp_qty = fill->fill_qty + sfill->fill_qty;
        double matched_fill_qty = 0.0;
        if (tmp_qty <= 0.0) {
          sfill->fill_qty = tmp_qty;
          matched_fill_qty = fill->fill_qty;
          fill->fill_qty = 0.0;
          CHECK_GT(matched_fill_qty, 0);
        } else {
          fill->fill_qty = tmp_qty;
          matched_fill_qty = -sfill->fill_qty;
          sfill->fill_qty = 0.0;
          CHECK_GT(matched_fill_qty, 0);
        }

        BuySellPair bsp;
        bsp.buy_timestamp = fill->timestamp;
        bsp.sell_timestamp = sfill->timestamp;
        bsp.fill_buy_price = fill->fill_price;
        bsp.fill_sell_price = sfill->fill_price;
        bsp.fill_qty = matched_fill_qty;
        bsp.pnl = (bsp.fill_sell_price - bsp.fill_buy_price) * bsp.fill_qty;
        bsp.pnl_bp = CalcPnlBp(bsp.fill_sell_price, bsp.fill_buy_price);
        // bsp_list_.push_back(bsp);
        CHECK_GE(bsp.pnl * bsp.pnl_bp, 0);

        auto proto = ToBuySellProto(bsp);
        buy_lose_qty_ += bsp.fill_qty;
        buy_lose_pnl_ += bsp.pnl;
        LOG(INFO) << "BuySellProto (buy-sell-match-lose): " << proto.DebugString();
        LOG(ERROR) << "buy_lose_qty: " << buy_lose_qty_ << ", buy_lose_pnl: " << buy_lose_pnl_;
      }
    }

    sell_fills_.erase(
        std::remove_if(
            sell_fills_.begin(),
            sell_fills_.end(),
            [this](const Fill* fill) {
              return this->is_qty_too_small(fill->fill_qty);
            }),
        sell_fills_.end());

    if (!IsClose(fill->fill_qty, 0.0)) {
      buy_fills_.push_back(fill);
      LOG(INFO) << "BuyFill added!";
      CHECK_GT(fill->fill_qty, 1e-10);

      auto compare = [](const Fill* lhs, const Fill* rhs) {
        CHECK_GT(lhs->fill_qty, 0);
        CHECK_GT(rhs->fill_qty, 0);
        return lhs->fill_price < rhs->fill_price;
      };
      std::sort(buy_fills_.begin(), buy_fills_.end(), compare);

      for (auto f : buy_fills_) {
        bfqty_ += f->fill_qty;
        bfamt_ += (f->fill_price * f->fill_qty);
      }
    }
  } else {  // Sell fill
    for (auto* bfill : buy_fills_) {
      if (IsClose(fill->fill_qty, 0.0)) {
        break;
      }

      if (fill->fill_price > bfill->fill_price * (1 + walk_step())) {
        double tmp_qty = fill->fill_qty + bfill->fill_qty;
        double matched_fill_qty = 0.0;
        if (tmp_qty >= 0.0) {
          bfill->fill_qty = tmp_qty;
          matched_fill_qty = -fill->fill_qty;
          fill->fill_qty = 0.0;
          CHECK_GT(matched_fill_qty, 0);
        } else {
          fill->fill_qty = tmp_qty;
          matched_fill_qty = bfill->fill_qty;
          bfill->fill_qty = 0.0;
          CHECK_GT(matched_fill_qty, 0);
        }

        BuySellPair bsp;
        bsp.buy_timestamp = bfill->timestamp;
        bsp.sell_timestamp = fill->timestamp;
        bsp.fill_buy_price = bfill->fill_price;
        bsp.fill_sell_price = fill->fill_price;
        bsp.fill_qty = matched_fill_qty;
        bsp.pnl = (bsp.fill_sell_price - bsp.fill_buy_price) * bsp.fill_qty;
        bsp.pnl_bp = CalcPnlBp(bsp.fill_sell_price, bsp.fill_buy_price);
        // bsp_list_.push_back(bsp);
        CHECK_GE(bsp.pnl * bsp.pnl_bp, 0);

        auto proto = ToBuySellProto(bsp);
        sell_profit_qty_ += bsp.fill_qty;
        sell_profit_pnl_ += bsp.pnl;
        LOG(INFO) << "BuySellProto (sell-buy-match-profit): " << proto.DebugString();
        LOG(ERROR) << "sell_profit_qty: " << sell_profit_qty_
            << ", sell_profit_pnl: " << sell_profit_pnl_;
      }
    }

    // matches that causes loss
    for (auto* bfill : buy_fills_) {
      // Ingore those that matched already.
      if (IsClose(bfill->fill_qty, 0.0)) {
        continue;
      }

      if (IsClose(fill->fill_qty, 0.0)) {
        break;
      }

      double tmp_qty = fill->fill_qty + bfill->fill_qty;
      double matched_fill_qty = 0.0;
      if (tmp_qty >= 0.0) {
        bfill->fill_qty = tmp_qty;
        matched_fill_qty = -fill->fill_qty;
        fill->fill_qty = 0.0;
        CHECK_GT(matched_fill_qty, 0);
      } else {
        fill->fill_qty = tmp_qty;
        matched_fill_qty = bfill->fill_qty;
        bfill->fill_qty = 0.0;
        CHECK_GT(matched_fill_qty, 0);
      }

      BuySellPair bsp;
      bsp.buy_timestamp = bfill->timestamp;
      bsp.sell_timestamp = fill->timestamp;
      bsp.fill_buy_price = bfill->fill_price;
      bsp.fill_sell_price = fill->fill_price;
      bsp.fill_qty = matched_fill_qty;
      bsp.pnl = (bsp.fill_sell_price - bsp.fill_buy_price) * bsp.fill_qty;
      bsp.pnl_bp = CalcPnlBp(bsp.fill_sell_price, bsp.fill_buy_price);
      // bsp_list_.push_back(bsp);
      CHECK_GE(bsp.pnl * bsp.pnl_bp, 0);

      auto proto = ToBuySellProto(bsp);
      sell_lose_qty_ += bsp.fill_qty;
      sell_lose_pnl_ += bsp.pnl;
      LOG(INFO) << "BuySellProto (sell-buy-match-lose): " << proto.DebugString();
      LOG(ERROR) << "sell_lose_qty: " << sell_lose_qty_ << ", sell_lose_pnl: " << sell_lose_pnl_;
    }

    buy_fills_.erase(
        std::remove_if(
            buy_fills_.begin(),
            buy_fills_.end(),
            [this](const Fill* fill) {
              return this->is_qty_too_small(fill->fill_qty);
            }),
        buy_fills_.end());

    if (!IsClose(fill->fill_qty, 0.0)) {
      CHECK_LT(fill->fill_qty, -1e-10);
      sell_fills_.push_back(fill);
      LOG(INFO) << "SellFill added!";
      auto compare = [](const Fill* lhs, const Fill* rhs) {
        CHECK_LT(lhs->fill_qty, 0);
        CHECK_LT(rhs->fill_qty, 0);
        return lhs->fill_price > rhs->fill_price;
      };
      std::sort(sell_fills_.begin(), sell_fills_.end(), compare);

      for (auto f : sell_fills_) {
        sfqty_ += f->fill_qty;
        sfamt_ += (f->fill_price * f->fill_qty);
      }
    }
  }
}

coin2::strategy::umm::FillStatsProto FillStats::DebugProto() const {
  coin2::strategy::umm::FillStatsProto proto;
  const double eps = 1e-10;

  proto.set_buy_qty(buy_qty_);
  proto.set_buy_amount(buy_amount_);
  proto.set_avg_buy_price(buy_amount_ / (buy_qty_ + eps));

  proto.set_sell_qty(sell_qty_);
  proto.set_sell_amount(sell_amount_);
  proto.set_avg_sell_price(sell_amount_ / (sell_qty_ + eps));

  auto* bbo = proto.mutable_bbo();
  bbo->set_ask0(bbo_.ask0);
  bbo->set_bid0(bbo_.bid0);
  bbo->set_midp(bbo_.midp);
  bbo->set_timestamp(bbo_.timestamp);
  bbo->set_product_id(bbo_.product_id);
  proto.set_position(position());
  proto.set_pnl(pnl());
  proto.set_fee(fee());

  for (const auto* fill : buy_fills_) {
    auto* bfill = proto.add_buy_fills();
    bfill->set_timestamp(fill->timestamp);
    bfill->set_fill_price(fill->fill_price);
    bfill->set_fill_qty(fill->fill_qty);
    bfill->set_product_id(fill->product_id);
    bfill->set_fee(fill->fee);
  }

  for (const auto* fill : sell_fills_) {
    auto* sfill = proto.add_sell_fills();
    sfill->set_timestamp(fill->timestamp);
    sfill->set_fill_price(fill->fill_price);
    sfill->set_fill_qty(fill->fill_qty);
    sfill->set_product_id(fill->product_id);
    sfill->set_fee(fill->fee);
  }

  /*
  for (const auto& bsp : bsp_list_) {
    auto* pair = proto.add_buy_sell_pairs();
    pair->set_buy_timestamp(bsp.buy_timestamp);
    pair->set_sell_timestamp(bsp.sell_timestamp);
    pair->set_fill_buy_price(bsp.fill_buy_price);
    pair->set_fill_sell_price(bsp.fill_sell_price);
    pair->set_fill_qty(bsp.fill_qty);
    pair->set_pnl(bsp.pnl);
    pair->set_pnl_bp(bsp.pnl_bp);
  }*/

  return proto;
}

std::string FillStats::DebugString() const {
  // debug string
  return DebugProto().DebugString();
}

}  // namespace xguo
